<?php
/**
 * Nordic Repository
 * 
 * LICENSE
 * 
 * The new BSD license is applied on this source-file. For further
 * information please visit http://license.nordic-dev.de/newbsd.txt
 * or send an email to andre.moelle@gmail.com.
 */

/**
 * Necessary includes.
 */
Zend_Loader::loadClass('Zend_Config');

/**
 * This class can parse native PHP-configs which are based upon arrays.
 * 
 * An example can look like this:
 * 
 * <code>
 * # content of "config.php"
 * return array(
 *	'foo : bar' => array(
 * 		'foo' => 42,
 * 		'dim' => 2
 * 	),
 * 	'bar : foobar' => array(
 * 		'bar' => 42,
 * 		'dim' => 1
 * 	),
 * 	'foobar' => array(
 * 		'foobar' => 42,
 * 		'dim' => 0
 * 	)
 * );
 * </code>
 * 
 * The order of the sections do not matter. With the colon ":" you can
 * inherit the content of another section. Please note, that the content
 * of the file is NOT surrounded by <?php and ?> respectively.
 * 
 * @category   Nordic
 * @package    Nordic_Config
 * @copyright  2007 Nordic Development
 * @license    http://license.nordic-dev.de/newbsd.txt (New-BSD license)
 * @author     Andre Moelle <andre.moelle@gmail.com>
 * @version    $Id: Php.php 3 2007-07-08 09:21:42Z andre.moelle $
 */
class Nordic_Config_Php extends Zend_Config
{
	/**
	 * Makes the config ready.
	 * 
	 * @throws Zend_Config_Exception
	 * @param string $filename file to load
	 * @param string $section section that should be used
	 * @param boolean $allowModification determines whether modifications are allowed
	 */
	public function __construct ($filename, $section, $allowModifications = false)
	{
		$content = $this->_parseArray($this->_readFile($filename), $section, $filename);
		
		parent::__construct($content, $allowModifications);
		$this->_loadedSection = $section;
	}
	
	/**
	 * Orders the dependencies in a logical order.
	 * 
	 * The result-array contains the keys ordered by priority in
	 * which they should be loaded. Therefore the sections without
	 * parents are the first in the list. Thereafter the keys of the
	 * child-elements follow.
	 * 
	 * <code>
	 * $inheritance = array(
	 * 	'foo' => 'bar',
	 * 	'bar' => 'foobar',
	 * 	'foobar' => false
	 * );
	 * 
	 * $result = array(
	 * 	'foobar',
	 * 	'bar',
	 * 	'foo'
	 * );
	 * </code>
	 * 
	 * @param array $inheritance sections and their parents
	 * @return array
	 */
	protected function _createOrder ($inheritance)
	{
		$result = array();
		
		while($inheritance)
		{
			foreach($inheritance as $name => $parent)
			{
				if(!$parent)
				{
					$value = 0;
				}
				else if(isset($result[$parent]))
				{
					$value = $result[$parent] + 1;
				}
				else
				{
					continue;
				}
				
				$result[$name] = $value;
				unset($inheritance[$name]);
			}
		}
		
		asort($result);
		
		return array_keys($result);
	}
	
	/**
	 * Filters all unnecessary sections.
	 * 
	 * To reduce the load which could be caused by huge configs, this
	 * method filters all elements, which does not belong to the wanted
	 * sections. If $section == false, the given array is returned
	 * unaltered.
	 * 
	 * @param array $array array which should be filterd
	 * @param mixed $section sections which the result should contain
	 * @param string $filename filename which is only necessary for exception-message
	 * @return array
	 */
	protected function _filter ($array, $section, $filename)
	{
		$result = array(
			array(),
			array()
		);
		
		if(!$section)
		{
			return $array;
		}
		
		$section = (array)$section;
		
		while($section)
		{
			foreach($section as $key => $sectionName)
			{
				if(!$sectionName)
				{
					continue;
				}
				if(!isset($array[0][$sectionName]))
				{
	                throw new Zend_Config_Exception("Section '$sectionName' cannot be found in $filename");
				}
				
				if($array[1][$sectionName])
				{
					$section[] = $array[1][$sectionName];
				}
				
				$result[0][$sectionName] = $array[0][$sectionName];
				$result[1][$sectionName] = $array[1][$sectionName];
				unset($section[$key]);
			}
		}
		
		return $result;
	}
	
	/**
	 * Merges two array recursively and returns the merged array.
	 * 
	 * The second array is more important than the first. If only one
	 * element with a specified index is set, the element is taken.
	 * If the second is a string, the other value is overwritten
	 * without asking. If the element of the second array is an
	 * array, the method tries to merge both values.
	 * 
	 * @param array $array first array
	 * @param array $array2 second array
	 * @return array
	 */
	protected function _mergeRecursively ($array, $array2)
	{
		$result = array();
		
		foreach(array_unique(array_merge(array_keys($array), array_keys($array2))) as $key)
		{
			if(!isset($array[$key]))
			{
				$result[$key] = $array2[$key];
			}
			else if(!isset($array2[$key]))
			{
				$result[$key] = $array[$key];
			}
			else if(!is_array($array2[$key]))
			{
				$result[$key] = $array2[$key];
			}
			else
			{
				$result[$key] = $this->_mergeRecursively(
					(array)$array[$key],
					(array)$array2[$key]
				);
			}
		}
		
		return $result;
	}
	
	/**
	 * Normalizes the content of the incoming config-array.
	 * 
	 * It resolves the dependencies of each element in the array. So
	 * the name of the section is normalized. The normalized content
	 * of the array is in the first dimension of the result-array while
	 * the second dimension contains the dependencies.
	 * 
	 * <code>
	 * # incoming array
	 * $array = array(
	 * 	'foo : bar' => array(
	 * 		'foo' => 42,
	 * 		'dim' => 2
	 * 	),
	 * 	'bar : foobar' => array(
	 * 		'bar' => 42,
	 * 		'dim' => 1
	 * 	),
	 * 	'foobar' => array(
	 * 		'foobar' => 42,
	 * 		'dim' => 0
	 * 	)
	 * );
	 * 
	 * # result-array
	 * $result = array(
	 * 	array(
	 * 		'foo' => array(
	 * 			'foo' => 42,
	 * 			'dim' => 2
	 * 		),
	 * 		'bar' => array(
	 * 			'bar' => 42,
	 * 			'dim' => 1
	 * 		),
	 * 		'foobar' => array(
	 * 			'foobar' => 42,
	 * 			'dim' => 0
	 * 		)
	 * 	),
	 * 	array(
	 * 		'foo' => 'bar',
	 * 		'bar' => 'foobar',
	 * 		'foobar' => false
	 * 	)
	 * );
	 * </code>
	 * 
	 * @param array $array array to normalize
	 * @return array
	 */
	protected function _normalize ($array)
	{
		$result = array(
			array(),
			array()
		);
		
		foreach($array as $key => $value)
		{
			if(strpos($key, ':') === false)
			{
				$parent = false;
			}
			else
			{
				list($key, $parent) = explode(':', $key, 2);
			}
			
			$parent = trim($parent);
			$key = trim($key);
			
			if($parent)
			{
				$this->_assertValidExtend($key, $parent);
			}
			
			$result[0][$key] = $value;
			$result[1][$key] = $parent;
		}
		
		return $result;
	}
	
	/**
	 * Parses the read array.
	 * 
	 * Returned is the parsed array with the inheritance.
	 * 
	 * @param array $array array to parse
	 * @param mixed $section sections to load
	 * @param string $filename filename of the loaded config
	 * @return array
	 */
	protected function _parseArray ($array, $section, $filename)
	{
		list($array, $inheritance) = $this->_filter(
			$this->_normalize($array, $section, $filename),
			$section,
			$filename
		);
		
		$temp = array();
		
		foreach($this->_createOrder($inheritance) as $key)
		{
			if(!$inheritance[$key])
			{
				$temp[$key] = $array[$key];
				continue;
			}
			
			$temp[$key] = $this->_mergeRecursively(
				$temp[$inheritance[$key]],
				$array[$key]
			);
		}
		
		if(!$section)
		{
			return $temp;
		}
		
		$section = (array)$section;
		$result = array();
		
		foreach(array_keys($temp) as $sectionName)
		{
			if(!in_array($sectionName, $section))
			{
				continue;
			}
			
			$result = array_merge($result, $temp[$sectionName]);
		}
		
		return $result;
	}
	
	/**
	 * Reads a file.
	 * 
	 * If the filename was not set or the file does not exist a
	 * "Zend_Config_Exception" will be thrown. Otherwise the
	 * read array is returned.
	 * 
	 * @throws Zend_Config_Exception
	 * @param string $filename file that should be loaded
	 * @return array
	 */
	protected function _readFile ($filename)
	{
		if(!$filename)
		{
			throw new Zend_Config_Exception('Filename is not set');
		}
		if(!file_exists($filename))
		{
			throw new Zend_Config_Exception(
				'File "' . $filename . '" does not exist'
			);
		}
		
		return eval(file_get_contents($filename));
	}
}